Un lenguaje de programación es un conjunto formal de instrucciones que permite a los humanos comunicarse con las computadoras para resolver problemas mediante algoritmos.
Un lenguaje de programación actúa como un puente entre el pensamiento humano y la lógica de la máquina. Consiste en vocabulario (palabras reservadas como if, for), gramática (reglas de sintaxis) y semántica (significado de las instrucciones).
Las computadoras solo entienden lenguaje máquina (binario: 0s y 1s), por lo que los compiladores (programa queda listo para ejecutarse directamente en un procesador específico) o intérpretes (Traducción dinámica en tiempo de ejecución (Just-In-Time, JIT)) traducen el código fuente a instrucciones ejecutables.
Características principales de cualquier lenguaje de programación:
Sintaxis: Reglas que definen cómo escribir el código correctamente.
Semántica: Lo que significa cada instrucción válida.
Paradigmas: Estructural (secuencial), procedural, orientado a objetos, funcional.
Python es un lenguaje de programación de alto nivel interpretado, multiparadigma y de propósito general creado a finales de los años 80 por Guido van Rossum. Se caracteriza por su énfasis en la legibilidad del código y la simplicidad de la sintaxis, convirtiendolo en una opción ideal tanto para principiantes como para desarrolladores experimentados.
De alto nivel: El lenguaje abstrae los detalles complejos de la máquina permitiendo que los programadores se concentren en resolver problemas específicos del dominio (en nuestro caso, finanzas).
Fácil de aprender: Su sintaxis simple permite escribir código más rápido y entenderlo mejor.
Interpretado: El código Python se ejecuta línea por línea a través de un intérprete, permitiendo desarrollo interactivo y prototipado rápido.
Dinámicamente tipado: Los tipos de datos se infieren en tiempo de ejecución, lo que reduce la verbosidad del código.
Multiparadigma: Soporta programación orientada a objetos, funcional, procedural e imperativa simultáneamente.
Multiplataforma: Funciona en Windows, macOS, Linux y otros sistemas operativos sin cambios en el código.
Código abierto: Es gratuito y cuenta con una gran comunidad que contribuye a su desarrollo.
El zen de Python
La filosofía de diseño de Python se resume en el “Zen de Python” (PEP 20), es un conjunto de 19 principios que guían el diseño del lenguaje Python y promueven la escritura de código limpio, legible y eficiente.
Pueden consultarse ejecutando el siguiente comando en cualquier intérprete Python:
import this
No osbstante, destacamos los siguientes:
“Beautiful is better than ugly”: El código bien escrito es más mantenible.
“Explicit is better than implicit”: La claridad es preferible a la magia.
“Simple is better than complex”: Las soluciones simples generalmente son mejores.
“Readability counts”: La legibilidad es crítica, el código es la documentación más actualizada y completa.
Deuda ténica
Metáfora usada en desarrollo de software para describir los costes futuros por tomar atajos o decisiones subóptimas (código sucio, mal estructurado,…) con el objetivo de acortar tiempos o entregar mas rápido.
Similar a una deuda financiera que acumula intereses, dificultando el mantenimiento y desarrollo futuro.
Comparativa con otros lenguajes
Aspecto
Python
R
C++
Java
Rust
% de uso1
57,9%
4,9%
23,5%
29.3%
14,8%
Tipo de Lenguaje
Multiparadigma
Funcional
Multiparadigma
Orientado a Objetos
Multiparadigma
Compilado/Interpretado
Interpretado
Interpretado
Compilado
Compilado (bytecode)
Compilado
Gestión de la memoria
Automatico (GC)
Automatico (GC)
Manual
Automatico (GC)
Compilado (Ownership)
Tipado
Dinámico
Dinámico
Estático
Estático
Estático
Curva de Aprendizaje
Medio
Medio
Muy Alta
Alta
Muy Alta
Velocidad Ejecución
Medio
Medio
Muy Rápido
Rápido
Muy Rápido
Velocidad Desarrollo
Muy Rápido
Rápido
Lento
Lento
Lento
Año de Creación
1991
1995
1983
1995
2010
Ventajas y Desventajas de Python
Como Musashi (2018) enseña en El Libro de los Cinco Anillos, no existe el arma perfecta universal: cada una (espada, lanza, arco,…) brilla en su contexto específico. La maestría radica en adaptarse a las circunstancias y elegir la herramienta adecuada para cada situación.
Ventaja
Impacto
Legibilidad
Código fácil de enteder
Prototipado rápido
Idea → Código en horas, no meses
Librerias disponibles
Amplio abanico de utilidades ya escritas
Gratuito
Sin coste
Producción
De prototipos a producción
Proposito general
Soluciones para casi cualquier problema
Desventaja
Severidad
Velocidad
Bucles lentos vs. C++ (1000x más lento)
Memoria
Mayor consumo para datasets masivos
Latencia
No apto para trading HFT (microsegundos)
GIL (Global Interpreter Lock)
Multithread limitado en CPU
Producción crítica
Necesita testing exhaustivo
“If you only have a hammer, everything looks like a nail”
¿Cómo programar en Python?
1. El intérprete
El componente central para programar en Python es el intérprete, que es el software que traduce y ejecuta tu código. Lo reconoceremos por los siguientes símbolos “>>>” o “…”
Instalar el interprete de Python
En Windows: Descarga el instalador ejecutable (.exe) a través de la página oficial
Durante la instalación, es importante añadir el directorio de instalación al “PATH”, lo que permite ejecutar Python desde cualquier terminal sin especificar la ruta completa. Una vez completada la instalación, verifica escribiendo en la línea de comandos:
python--version# Versión del interpretewhere python # Ruta del intérprete
2. El IDE
Un editor de código o IDE es la herramienta donde escribirás tu código Python. Las opciones varían según tu nivel y necesidades:
pip (Package Installer for Python) es la herramienta estándar para instalar librerías y paquetes adicionales. Viene incluido automáticamente con Python
python-m pip install numpy pandaspip install numpy pandaspip list
Al ejecutar el comando pip install el gestor de paquetes pip buscará en los repositorios públicos, si existe lo descargará, junto a sus dependencias.
Python
Agenda:
Introducción a la programación en Python
Python
QuantLib
Valoración de derivados financieros
Modelo multifactorial de riesgo de crédito
Intérprete de Python
Programa que lee, traduce y ejecuta el código línea por línea en tiempo real, actuando como puente entre tu lógica y la máquina
También conocido como REPL (Read-Eval-Print Loop), opera en un ciclo continuo: lee tu código, lo evalúa, imprime el resultado y espera la siguiente instrucción. Cuando ejecutas python en la terminal, inicia una sesión interactiva que muestra el prompt primario >>> para comandos nuevos y el prompt secundario ... para líneas continuas, como bucles o funciones.
$ pythonPython 3.12.3 (main, Jun 18 2025, 17:59:45)[GCC 13.3.0] on linuxType"help", "copyright", "credits" or "license" for more information.>>> quit()
El intérprete muestra información de versión y copyright, luego el prompt >>>. Para salir, usa: quit(), exit() o Ctrl+C (Linux/macOS) / Ctrl+Z (Windows).
Lectura (Read): Captura tu línea de código.
Evaluación (Eval): Compila a bytecode y ejecuta si el código es válido, si no lanzará una excepción.
Impresión (Print): Muestra el resultado.
Bucle (Loop): Vuelve al prompt >>>.
Durante la evaluación el intérprete ignora los denominados comentarios, son anotaciones en el código para documentar, explicar lógica o desactivar temporalmente líneas sin generar errores. Se crean con el símbolo # al inicio de una línea o después del código. Para bloques multilínea, se usan comillas triples (’’’ o “““) sin asignar a variable
# Esto es un comentario ignorado por Pythonprint("Hola Mundo") # A partir de '#' el interprete no lo procesa'''Este es un comentario multinea:Los comentarios mejoran la legibilidad y mantenimiento del código.Las buenas prácticas recomiendan comentarios concisos, actualizados y enfocados en el "porqué" más que en el "qué".'''
Operadores matemáticos
El intérprete actua como una simple calculadora. Por ejemplo:
2+250-5*6(50-5*6) /45**217/3# division clasiva 17//3# operador //: cociente sin la parte decimal17%3# operador %: modulo5*3+2# cociente * divisor + restowidth =20height =5*9width * heightx, y =10, 20# Asignación múltiplea = b = c =0# Dar mismo valor a varias variables
Operadores suma, resta, multiplicación y división
Podemos ejecutar cualquier operación (+, -,*, /) y nos devolverá el resultado. Además podemos usar paréntesis (()) para agrupar operaciones
El signo igual (=) se usa para asignar un valor a una variable. Cuando asignamos una o múltiples variables, el interprete no mostrará ningun resultado, antes del siguiente prompt interactivo:
No obstante, existen 35 términos predefinidos que el intérprete reserva exclusivamente para su sintaxis y control de flujo, impidiendo su uso como nombres de variables, funciones o identificadores para evitar conflictos. Pueden ser consultadas ejecutando el siguiente comando:
import keywordkeyword.kwlist
Comparación y operadores lógicos
Hay tres operadores lógicos en Python: and, or y not, sirven para evaluar expresiones booleanas y permiten combinar condiciones en estructuras de control como if o bucles.
Operador and (&)
Devuelve True solo si ambos operandos son verdaderos, evaluación en cortocircuito. Ejemplo: True and False resulta en False.
Operador or (|)
Retorna True si al menos un operando es verdadero, también con cortocircuito. Ejemplo: True or False devuelve True.
Operador not (!)
Invierte el valor booleano del operando: not True es False, y viceversa; tiene mayor precedencia que and y or (orden: not > and > or). Los operadores de comparación en Python, como “greater than” (>), complementan a los lógicos al generar valores booleanos para condiciones.
Operadores básicos
\(>\) (mayor que): 5 > 3 es True.
\(<\) (menor que): 3 < 5 es True.
\(>=\) (mayor o igual): 5 >= 5 es True.
\(<=\) (menor o igual): 3 <= 5 es True.
Operadores de Igualdad
\(==\) (igual): 5 == 5 es True.
\(!=\) (diferente): 5 != 3 es True.
Uso Combinado
Se integran con and, or y not en expresiones como if x > 10 and y <= 20:, evaluando precedencia de izquierda a derecha con cortocircuito.
# La expresión if en Python permite condicionar la ejecucion del programa.# Ejecuta el codigo si la condicion se cumple, si no ejecuta la sentencia despues del else# Se puede encadenar con if - elif - elsenota =75print("Aprobado") if nota >=50elseprint("Suspenso")
Tipos de datos: básicos
Como dijimos anteriormente, Python es un lenguaje dinamicante tipado, el intérprete se encarga de inferir el tipo del objeto en tiempo de ejecución. Al contrario que en los lenguajes compilados como C++ o Rust en los que generalemente su tipado es estático (definido por el desarrollador) o inferido en tiempo de compilación. Ver (Python Software Foundation, 2025) para tipos de datos mas espcializados.
Tipo
Significado
Uso
int
Integer
Números naturales
float
Número con punto flotante
Números reales
bool
Booleano
Valores binarios (Verdadero/Falso)
str
String
Caracteres, palabras, texto, …
Integer (int) - representa números enteros, valores numéricos sin parte decimal, pueden ser positivos o negativos. En Python 3 los enteros tienen precisión arbitraria; no tienen un límite de tamaño fijo (overflow).
Contadores e índices: Controlar bucles (for, while) y acceder a posiciones en listas o bases de datos.
Matemáticas discretas: Representar cantidades indivisibles (ej. número de acciones, personas, artículos en inventario).
Tipos de datos: básicos
Como dijimos anteriormente, Python es un lenguaje dinamicante tipado, el intérprete se encarga de inferir el tipo del objeto en tiempo de ejecución. Al contrario que en los lenguajes compilados como C++ o Rust en los que generalemente su tipado es estático (definido por el desarrollador) o inferido en tiempo de compilación. Ver (Python Software Foundation, 2025) para tipos de datos mas espcializados.
Tipo
Significado
Uso
int
Integer
Números naturales
float
Número con punto flotante
Números reales
bool
Booleano
Valores binarios (Verdadero/Falso)
str
String
Caracteres, palabras, texto, …
Número con punto flotante (float) - se utiliza para representar números reales con parte decimal. En Python, estos se implementan generalmente como valores de “doble precisión”.
Cálculos matematicos y estadísticos: Medidas continuas como temperatura, distancias, pesos o probabilidades.
Representación de magnitudas: precios, tasas de interés o porcentajes.
Tipos de datos: básicos
Como dijimos anteriormente, Python es un lenguaje dinamicante tipado, el intérprete se encarga de inferir el tipo del objeto en tiempo de ejecución. Al contrario que en los lenguajes compilados como C++ o Rust en los que generalemente su tipado es estático (definido por el desarrollador) o inferido en tiempo de compilación. Ver (Python Software Foundation, 2025) para tipos de datos mas espcializados.
Tipo
Significado
Uso
int
Integer
Números naturales
float
Número con punto flotante
Números reales
bool
Booleano
Valores binarios (Verdadero/Falso)
str
String
Caracteres, palabras, texto, …
Booleano (bool) - solo puede tomar dos valores: True (Verdadero) o False (Falso). Es una subclase de int en Python (donde True se comporta como 1 y False como 0 en contextos aritméticos).
Control de flujo: Determinar qué camino toma el código en sentencias if, while o filtros de datos.
Indicar estados binarios (ej. is_active, has_error, market_open).
Validaciones: Resultados de comparaciones (ej. precio > 100 devuelve un bool).
Tipos de datos: básicos
Como dijimos anteriormente, Python es un lenguaje dinamicante tipado, el intérprete se encarga de inferir el tipo del objeto en tiempo de ejecución. Al contrario que en los lenguajes compilados como C++ o Rust en los que generalemente su tipado es estático (definido por el desarrollador) o inferido en tiempo de compilación. Ver (Python Software Foundation, 2025) para tipos de datos mas espcializados.
Tipo
Significado
Uso
int
Integer
Números naturales
float
Número con punto flotante
Números reales
bool
Booleano
Valores binarios (Verdadero/Falso)
str
String
Caracteres, palabras, texto, …
String (str) - Es una secuencia inmutable de caracteres. Se delimitan con comillas simples (’ ’) o dobles (” “). Al ser inmutables, no se pueden modificar en el mismo espacio de memoria una vez creados; cualquier”cambio” genera una nueva cadena.
Procesamiento de datos: Almacenamiento de nombres, direcciones, códigos, y lectura de archivos CSV/JSON.
Interacción con usuario: Mensajes mostrados en pantalla o logs de errores.
Identificadores: Claves en diccionarios o bases de datos no numéricas.
Tipos de datos: básicos
1. None (Valores nulos u opcionales)
None es el objeto singleton que utiliza Python para representar la ausencia de valor o un estado nulo. Se usa para inicializar variables vacías o como valor por defecto en funciones. Se compara con is no con ==
x =Noneif x isNone:print("x no tiene valor")
2. type()
La función type(obj) devuelve la clase exacta del objeto obj
x =1type(x) # <class 'int'>
3. Conversion de tipos (Casting)
Usamos los constructores de clase para convertir entre tipos: int(), float(), str(), list().
Python es fuertemente tipado pero dinámico. No hace “casting implícito” que pierda datos (como sumar “5” + 5 dará error), por lo que debemos convertir explícitamente.
precio ="50"total =int(precio) +10# Conversión explícita de str a intprecio +10# error
Estructuras de datos (Colecciones)
Son estructuras de datos que permiten almacenar y organizar múltiples elementos, agrupándolos en tipos especializados. Todas admiten elementos heterogéneos, son iterables e indexables (excepto sets), y ofrecen métodos optimizados para agregar, eliminar o buscar datos según su naturaleza mutable/inmutable. Ver (Python Software Foundation, 2025).
Tipo
Mutable / Inmutable
Uso
list
Mutable
Conjunto variable de objetos
tuple
Inmutable
Conjunto fijo de objetos
dict
Mutable
Almacenamiento clave-valor
set
Mutable
Conjunto: Colección de objetos únicos
Lista (list) - Secuencias mutables y ordenadas, definidas con [] o list(), usadas para datos modificables. Pueden contener objetos de distintos tipos (enteros, strings, incluso otras listas) mezclados.
Mantienen el orden de inserción.
Permiten elementos duplicados.
Sus elementos pueden modificarse, añadirse o eliminarse dinámicamente.
Estructuras de datos (Colecciones)
Son estructuras de datos que permiten almacenar y organizar múltiples elementos, agrupándolos en tipos especializados. Todas admiten elementos heterogéneos, son iterables e indexables (excepto sets), y ofrecen métodos optimizados para agregar, eliminar o buscar datos según su naturaleza mutable/inmutable. Ver (Python Software Foundation, 2025).
Tipo
Mutable / Inmutable
Uso
list
Mutable
Conjunto variable de objetos
tuple
Inmutable
Conjunto fijo de objetos
dict
Mutable
Almacenamiento clave-valor
set
Mutable
Conjunto: Colección de objetos únicos
Tupla (tuple) - Secuencias inmutables y ordenadas, definidas con () o tuple(). Una vez creada, no puedes añadir, borrar ni cambiar sus elementos. Esto las hace más ligeras en memoria y seguras contra modificaciones accidentales.
Son más rápidas de procesar que las listas.
Se pueden usar como claves en diccionarios (las listas no, al ser mutables).
Estructuras de datos (Colecciones)
Son estructuras de datos que permiten almacenar y organizar múltiples elementos, agrupándolos en tipos especializados. Todas admiten elementos heterogéneos, son iterables e indexables (excepto sets), y ofrecen métodos optimizados para agregar, eliminar o buscar datos según su naturaleza mutable/inmutable. Ver (Python Software Foundation, 2025).
Tipo
Mutable / Inmutable
Uso
list
Mutable
Conjunto variable de objetos
tuple
Inmutable
Conjunto fijo de objetos
dict
Mutable
Almacenamiento clave-valor
set
Mutable
Conjunto: Colección de objetos únicos
Diccionario (dict) - Pares clave-valor no ordenados, como {"nombre": "Ana", "edad": 25}, definidas como: {clave: valor} o dict():
Las claves deben ser únicas e inmutables (strings, números, tuplas).
Acceso extremadamente rápido a los valores (complejidad O(1) promedio).
Estructuras de datos (Colecciones)
Son estructuras de datos que permiten almacenar y organizar múltiples elementos, agrupándolos en tipos especializados. Todas admiten elementos heterogéneos, son iterables e indexables (excepto sets), y ofrecen métodos optimizados para agregar, eliminar o buscar datos según su naturaleza mutable/inmutable. Ver (Python Software Foundation, 2025).
Tipo
Mutable / Inmutable
Uso
list
Mutable
Conjunto variable de objetos
tuple
Inmutable
Conjunto fijo de objetos
dict
Mutable
Almacenamiento clave-valor
set
Mutable
Conjunto: colección de objetos únicos
Conjuntos (set) - Estructuras de datos no ordenadas sin duplicados ni indexación, definidas con {valor1, valor2} o set(), útiles para operaciones únicas.
Elimina duplicados.
Altamente optimizado para operaciones matemáticas de conjuntos (unión, intersección, diferencia).
Acceso rápido para verificar pertenencia (if x in mi_set).
Blucles (Loops)
Los loops (bucles) son estructuras de control que permiten ejecutar un bloque de código múltiples veces, iterando sobre secuencias o bajo condiciones específicas los principales son: for y while.
Loop for
Permite especificar de antemano el número de iteraciones. Se define con for seguido de la variable receptora, la palabra in, la secuencia iterable, dos puntos : y el cuerpo indentado del bloque a ejecutar.
# Iterar sobre listafrutas = ["manzana", "plátano", "naranja"]for it_fruta in frutas:print(it_fruta)# Iterar sobre rangofor it_range inrange(5): # 0, 1, 2, 3, 4print(f"Número: {it_range}")# Iterar sobre stringfor it_letra in"Python":print(it_letra)# Iterar con índicefor it_indice, it_fruta inenumerate(frutas):print(f"{it_indice}: {it_fruta}")# Iterar sobre múltiples secuencias simultáneamentenombres = ["Ana", "Bob", "Carlos"]edades = [25, 30, 35]for it_nombre, it_edad inzip(nombres, edades):print(f"{nombre} tiene {edad} años")
# Crear lista de cuadrados[x**2for x inrange(5)] # [0, 1, 4, 9, 16]# Con condición[x for x inrange(10) if x %2==0] # [0, 2, 4, 6, 8]# Comprensión de diccionario{x: x**2for x inrange(4)} # {0: 0, 1: 1, 2: 4, 3: 9}# Iterar sobre diccionariospersona = {"nombre": "Ana", "edad": 25, "ciudad": "Madrid"}# Iterar sobre clavesfor it_clave in persona:print(it_clave)# Iterar sobre valoresfor it_valor in persona.values():print(it_valor)# Iterar sobre pares clave-valorfor it_clave, it_valor in persona.items():print(f"{it_clave}: {it_valor}")
Blucles (Loops)
Los loops (bucles) son estructuras de control que permiten ejecutar un bloque de código múltiples veces, iterando sobre secuencias o bajo condiciones específicas los principales son: for y while.
Loop while
Permite ejecutar un bloque de código repetidamente mientras se cumpla una condición específica. Se define con while seguido de la condición booleana, dos puntos : y el cuerpo indentado que se repite hasta que la condición sea falsa.
for i inrange(10):if i ==5:break# Sale del loopprint(i) # Output: 0, 1, 2, 3, 4
continue: Salta la iteración actual y continúa con la siguiente:
for i inrange(5):if i ==2:continue# Salta i=2print(i) # Output: 0, 1, 3, 4
else: Se ejecuta cuando el loop termina sin break:
for i inrange(5):print(i)else:print("Loop completado sin break")
Funciones
Las funciones son bloques de código que pueden ser reutilizados y que realizan una tarea específica, pueden recibir parámetros como entrada y pueden devolver resultados. Las ventajas de usar funciones son: reutilización (escribir código una sola vez, usarlo muchas veces), modularidad (dividir problemas complejos en tareas simples), mantenibilidad (cambios centralizados en una ubicación) y legibilidad (código más organizado y fácil de entender).
Una función se define con la palabra reservada def seguida del nombre de la función, paréntesis con parámetros opcionales y dos puntos, iniciando un bloque indentado con la lógica a ejecutar.
Componentes:
Nombre: identificador único de la función (ej: sumar)
Parámetros: variables de entrada (ej: a, b)
Docstring: (Opcional) descripción entre triples comillas “““…”“”
Cuerpo: lógica que ejecuta la función
Return: termina la ejecución, y define el valor devuelto (devuelve None si no se devuelve nada)
def sumar(a, b): c = a + breturn cprint(sumar(5, 3)) # Output: 8
Otra forma de definirlas es a través de funciones anónimas: funciones sin nombre que se define en una sola línea usando la palabra reservada lambda.
sumar =lambda x, y: x + y
Parametros y argumentos
Los parámetros de una función son variables que reciben valores (argumentos) cuando se invoca, permitiendo que la función procese datos de entrada y realice operaciones específicas sobre ellos.
Tipos de parámetros
Posicionales: Se asignan por orden de aparición; el orden importa
Valores por defecto: Tienen un valor predefinido si no se proporciona argumento
Nombrados: Se pasan especificando el nombre del parámetro, ignorando el orden:
def saludar(nombre, mensaje="Hola"):returnf"{mensaje}, {nombre}"print(saludar("Ana")) # Posicional + valor por defecto - Output: Hola, Anaprint(saludar("Ana", "Buenos días")) # Posicional sin valor por defecto - Output: Buenos días, Anaprint(saludar(mensaje="Buenos días", nombre="Ana")) # Nombrados
*args (Argumentos Variables Posicionales): Acepta cualquier número de argumentos posicionales como tupla:
** kwargs (Argumentos Variables Nombrados) : Acepta cualquier número de argumentos clave-valor como diccionario:
def mostrar_info(**datos):for clave, valor in datos.items():print(f"{clave}: {valor}")mostrar_info(nombre="Ana", edad=30, profesión="Ingeniera")
Ejercicio: Capitalización
Dado un capital incial (\(C\)), una tasa interes anual (\(r\)) y el tiempo de la inversion en años (\(t\)). Implementa la lógica para calcular el Valor Futuro (\(FV\)) usando un tipo de capitalización: interés simple, compuesto o continuo (opcional), utilizando las siguientes fórmulas:
Dado un capital incial (\(C\)), una tasa interes anual (\(r\)) y el tiempo de la inversion en años (\(t\)). Implementa la lógica para calcular el Valor Futuro (\(FV\)) usando un tipo de capitalización: interés simple, compuesto o continuo (opcional), utilizando las siguientes fórmulas:
Escribir una funcion que genere el numero \(i\) de la serie de Fibonacci.
En la serie de Fibonacci el número \(i\) es la número es la suma de los dos anteriores.
La secuencia comienza con los números 0 y 1. Es decir, \(F(0) = 0\) y \(F(1) = 1\)
A partir de ahí, el siguiente número se calcula sumando los dos anteriores:
\(F_i = F_{i−1} + F_{i−2}\)
Serie de Fibonnaci: \(0, 1, 1, 2, 3, 5, 8, 13, 21, 34...\)
def fibonacci(...): ...for it inrange(0, 100):print(fibonacci(it))
Ejercicio: Serie de Fibonacci
Escribir una funcion que genere el numero \(i\) de la serie de Fibonacci.
En la serie de Fibonacci el número \(i\) es la número es la suma de los dos anteriores.
La secuencia comienza con los números 0 y 1. Es decir, \(F(0) = 0\) y \(F(1) = 1\)
A partir de ahí, el siguiente número se calcula sumando los dos anteriores:
\(F_i = F_{i−1} + F_{i−2}\)
Serie de Fibonnaci: \(0, 1, 1, 2, 3, 5, 8, 13, 21, 34...\)
# Opcion a:def fibonacci(i):if i ==0: return0if i ==1: return1 a, b =0, 1for it inrange(2, i +1): a = b b = a + breturn b# Opcion b:def fibonacci(i):if i ==0: return0if i ==1: return1 resultado = fibonacci(i -1) + fibonacci(i -2)return resultadofor it inrange(0, 100):print(fibonacci(it))
Aunque la definición matemática es recursiva (\(F(i)=F(i−1)+F(i−2)\)), programarla con un bucle (for) tiene complejidad lineal \(O(n)\).
Si usaramos recursión simple:
return fibonacci(i-1) + fibonacci(i-2)
la complejidad es exponencial, haciendo que calcular el término 50 tarde mucho tiempo, ya que reevaluamos lo evaluado.
Ejercicio: Serie de Fibonacci
Escribir una funcion que genere el numero \(i\) de la serie de Fibonacci.
En la serie de Fibonacci el número \(i\) es la número es la suma de los dos anteriores.
La secuencia comienza con los números 0 y 1. Es decir, \(F(0) = 0\) y \(F(1) = 1\)
A partir de ahí, el siguiente número se calcula sumando los dos anteriores:
\(F_i = F_{i−1} + F_{i−2}\)
Serie de Fibonnaci: \(0, 1, 1, 2, 3, 5, 8, 13, 21, 34...\)
def fibonacci(i, cache = {0: 0, 1: 1}):if i in cache:return cache[i] resultado = fibonacci(i -1, cache) + fibonacci(i -2, cache) cache[i] = resultadoreturn resultadofor it inrange(0, 100):print(fibonacci(it))
Esta técnica se conoce técnicamente como Memoización (Memoization). Al almacenar los resultados previos, transformamos un algoritmo de complejidad exponencial \(O(2^n)\) a uno lineal \(O(n)\), haciendo viable la recursividad para números grandes.
Ejercicio: Integral numerica
Escribe un programa en Python que calcule numericamente la integral definida de la función \(f(x) = x ^ 2 · sin(x)\) en el intervalo [a, b]:
\[
\int^a_b x^2 · sin(x) dx
\]
En muchos problemas de finanzas, nos encontramos con funciones que son difíciles o imposibles de integrar analíticamente (con lápiz y papel). En estos casos, aproximamos el área bajo la curva dividiendo el intervalo en pequeños rectángulos (Suma de Riemann). El ancho de todas las barras sera igual mientras que a altura de cada barra la determina el valor de la función en ese punto.
Definir la función en python
Implementar la función integral_riemann(f, a, b, n)
Algoritmo (Suma del area de rectángulos):
Calcula el ancho de cada rectángulo: \(\Delta x= \frac{b-a}{n}\).
Itera n veces. En cada paso:
Calcula la coordenada x del lado izquierdo del rectangulo: \(x_i=a+i·\Delta x\).
Calcula la altura del rectángulo evaluando la función: \(f(x_i)\).
Suma el área del rectangulo (\(base · altura\)).
Ejercicio: Integral numerica
import mathdef f(x):return (x**2) * math.sin(x)def integral_izquierda(a, b, n):""" Calcula la integral usando Suma de Riemann (Extremo Izquierdo). La altura del rectángulo se define por el valor de f(x) al inicio (izquierda) de cada sub-intervalo. """ ancho = (b - a) / n suma_areas =0.0for i inrange(n):# Usamos el borde izquierdo del rectángulo 'i' x = a + ancho * i altura = f(x) suma_areas += ancho * alturareturn suma_areasa =0b = math.pivalor_real =5.869604401# (pi^2 - 4)integral_izquierda(a, b, 10)integral_izquierda(a, b, 100)integral_izquierda(a, b, 1000)integral_izquierda(a, b, 1000000)
Librerias y módulos
Las librerías y módulos son colecciones de código reutilizable que agrupan funciones, clases y variables relacionadas, permitiendo extender la funcionalidad del lenguaje sin reescribir código desde cero.
Módulo
Archivo .py que contiene código Python (funciones, clases, variables) que puede importarse a través de la palabra reservado import y reutilizarse en otros programas:
# archivo: matematica.pydef sumar(a, b):return a + bdef restar(a, b):return a - bPI =3.14159
Librería (Package)
Colección organizada de módulos en directorios con estructura jerárquica, facilitando la gestión de proyectos grandes.
Operaciones matemáticas como raíces cuadradas (sqrt()), funciones trigonométricas (sin(), cos(), tan()), logaritmos, exponenciales y constantes matemáticas fundamentales como pi y e, además de funciones para números enteros como factorial() y gcd().
Genera números pseudoaleatorios, incluyendo enteros (randint()), flotantes uniformes (random()), selección aleatoria de elementos (choice()), muestreo (sample()) y mezclado de listas (shuffle()).
Fechas, horas y duraciones, permitiendo crear objetos de fecha/hora (datetime.now()), calcular diferencias (timedelta), formatear fechas (strftime()), instanciar fechas a partir de strings (strptime()) y realizar operaciones de calendario.
Ofrece interacción con el sistema operativo para operaciones de archivos y directorios como obtener el directorio actual (getcwd()), listar contenido (listdir()), crear/eliminar carpetas (mkdir(), rmdir()) y navegar rutas (path.join()).
Las clases en Python son plantillas para crear objetos con el objetivo de extender los tipos básicos que contienen atributos (datos) y métodos (funciones). Es un paradigma de programación que permite encapsular y representar nuestro problemas en unidades lógicas.
Cuando definimos una clase, el término self hace referencia al objeto actual, permitiendo acceder a sus atributos y métodos sin estar instanciado.
class Persona:"""Clase que representa una persona"""def__init__(self, nombre, edad):"""Constructor: inicializa atributos"""self.nombre = nombreself.edad = edaddef saludar(self, apellido =""):"""Método: función dentro de la clase"""returnf"Hola, soy {self.nombre}{apellido}"# Crear objeto (instancia)persona1 = Persona("Ana", 25)print(persona1.saludar("Martinez")) # Output: Hola, soy Ana Martinez
Definición de la clase
Una clase se define con la palabra reservada class seguido del nombre, :, y a continuación se declaran los atributos y métodos usando la indentación:
Atributos
Variables que pertenecen a la clase, almacenando datos del objeto:
Podemos acceder a sus atributos a través de self.nombre desde dentro de la clase o usando . despues del objeto instanciado objeto.nombre desde fuera de la clase.
Métodos
Son funciones que “viven” dentro de la clase. Permite que los objetos no solo almacenen datos (atributos), sino que también hagan cosas con ellos.
De igual manera que los atributos, podemos acceder a sus atributos a través de self.saludar() desde dentro de la clase o usando . despues del objeto instanciado objeto.saludar() desde fuera de la clase.
Constructor (init)
El constructor es un método especial que se ejecuta al crear una instancia, inicializando atributos.
Se declara usando def __init__():. Y define como se va a instanciar el objeto y los parametros que necesita.
Ventajas de las Clases
Encapsulación: Agrupar datos y funcionalidad relacionada
Reutilización: Crear múltiples objetos de la misma clase
Organización: Código más legible y mantenible
Herencia: Las subclases heredan propiedades de clases padre
Ejercicio: Riesgo de crédito
Implementar una clase que represente una única exposición crediticia y encapsule la lógica para calcular la pérdida esperada y capital regulatorio (fórmula de Vasicek).
Inicializar los 4 parámetros de riesgo como atributos del objeto.:
donde: \(\alpha\) es el nivel de confianza (por defecto, 99.9%), \(\Phi\) es la distribución normal estándar acumulada norm.cdf) de la libreria from scipy.stats import norm y \(\Phi^{-1}\) es la inversa de la normal estándar norm.cdf.
from datetime import datetimeimport mathclass ZeroCouponBond:def__init__(self, nominal, maturity_date):self.nominal =float(nominal)self.maturity = maturity_datedef npv(self, rate, valuation_date=None):# 1. Determinar fecha de valoración val_date = datetime.now() if valuation_date isNoneelse valuation_dateifnotisinstance(val_date, datetime):return0# si ya venció, el valor presente es 0 (o ya se pagó)if val_date >=self.maturity:return0.0 dias_a_vencimiento = (self.maturity - val_date).days T = dias_a_vencimiento /365.0# NPV = N / (1 + r)^(T)returnself.nominal / ((1+ rate) ** T)
La herencia es un mecanismo que permite definir una nueva clase a partir de otra existente, de forma que la clase derivada reutiliza, amplía o modifica los atributos y métodos de la clase base. La idea es modelar una relación “es-un” (por ejemplo, Coche es un Vehículo), donde la clase más general aporta el comportamiento común y las clases más específicas añaden o especializan funcionalidades, reduciendo duplicación de código y creando jerarquías más claras y mantenibles.
class Animal:"""Clase base (superclase)"""def__init__(self, nombre):self.nombre = nombredef hacer_sonido(self):return"Sonido genérico"class Perro(Animal):"""Clase derivada (subclase) hereda de Animal"""pass# Usoperro = Perro("Max")print(perro.nombre)# Output: Max (heredado)print(perro.hacer_sonido())# Output: Sonido genérico (heredado)
La herencia se define colocando entre paréntesis el nombre de la clase padre inmediatamente después del nombre de la clase hija en la declaración de la clase.
La subclase automáticamente recibe todos los métodos y atributos de la clase padre, pudiendo sobreescribirlos o extenderlos según sea necesario.
Herencias
La herencia es un mecanismo que permite definir una nueva clase a partir de otra existente, de forma que la clase derivada reutiliza, amplía o modifica los atributos y métodos de la clase base. La idea es modelar una relación “es-un” (por ejemplo, Coche es un Vehículo), donde la clase más general aporta el comportamiento común y las clases más específicas añaden o especializan funcionalidades, reduciendo duplicación de código y creando jerarquías más claras y mantenibles.
La herencia se define colocando entre paréntesis el nombre de la clase base inmediatamente después del nombre de la clase derivada en la declaración de la clase.
La subclase automáticamente recibe todos los métodos y atributos de la clase base, pudiendo sobreescribirlos o extenderlos según sea necesario.
Herencias
La herencia es un mecanismo que permite definir una nueva clase a partir de otra existente, de forma que la clase derivada reutiliza, amplía o modifica los atributos y métodos de la clase base. La idea es modelar una relación “es-un” (por ejemplo, Coche es un Vehículo), donde la clase más general aporta el comportamiento común y las clases más específicas añaden o especializan funcionalidades, reduciendo duplicación de código y creando jerarquías más claras y mantenibles.
from typing import Unionclass Punto2D:""" Representa un punto en el plano cartesiano 2D. """def__init__(self, x=0, y=0) ->None:"""Constructor: crea punto con coordenadas x, y"""self.x =float(x)self.y =float(y)def__repr__(self) ->str:"""Representación oficial para debug/reconstrucción"""returnf'Punto2D({self.x}, {self.y})'def__str__(self) ->str:"""Representación legible para usuario final"""returnf'({self.x}, {self.y})'def__eq__(self, other) ->bool:"""Comparación de igualdad"""ifnotisinstance(other, Punto2D):returnNotImplementedreturnself.x == other.x andself.y == other.ydef__lt__(self, other) ->bool:"""Menor que: orden lexicográfico (primero x, luego y)"""ifnotisinstance(other, Punto2D):returnNotImplementedreturn (self.x, self.y) < (other.x, other.y)def__len__(self) ->int:"""Número de dimensiones (siempre 2)"""return2def__bool__(self) ->bool:"""Verdadero si no es el origen (0,0)"""returnself.x !=0orself.y !=0def__add__(self, other: "Point2D") ->"Point2D":"""Suma vectorial: p1 + p2"""ifisinstance(other, Punto2D):return Punto2D(self.x + other.x, self.y + other.y)returnNotImplementeddef__sub__(self, other: "Point2D") ->"Point2D":"""Resta vectorial: p1 - p2"""ifisinstance(other, Punto2D):return Punto2D(self.x - other.x, self.y - other.y)returnNotImplementeddef__mul__(self, escalar: Union[int, float]) ->"Point2D":"""Multiplicación por escalar"""ifisinstance(escalar, (int, float)):return Punto2D(self.x * escalar, self.y * escalar)returnNotImplementeddef__getitem__(self, indice):"""Acceso por índice: p[0] = x, p[1] = y"""if indice ==0:returnself.xelif indice ==1:returnself.yraiseIndexError("Índice fuera de rango (0 o 1)")def distancia_origen(self):"""Distancia al origen (0,0)"""return (self.x **2+self.y **2) **0.5
En una clase de Python hay muchos métodos “dunder” (o mágicos), que son convenciones que Python usa para llamar a métodos especiales y atributos
Identidad y ciclo de vida
__init__(self, ...): constructor; inicializa el estado del objeto al crearlo.
__del__(self) (poco recomendable en la práctica): se ejecuta justo antes de que el objeto sea destruido, normalmente no se usa salvo casos muy específicos.
Representación del objeto
__repr__(self): representación “oficial”, pensada para debug y para que, si es posible, sea un string que permita reconstruir el objeto (Point2D(1, 2)). Se usa en repr(x) y en muchas shells interactivas.
__str__(self): representación “amigable” para usuarios finales; se usa en str(x) y print(x) cuando está definido, y normalmente es más legible que __repr__.
Las librerías externas amplían las capacidades del lenguaje para dominios especializados como análisis de datos, calculo matricial y modelado financiero. Requieren instalación previa con pip (gestor de paquetes):
NumPy proporciona arrays multidimensionales y operaciones matemáticas vectorizadas para cálculos numéricos de mayor rendimiento, evitando los loops de Python que son maslentos. Soporta álgebra lineal, estadística descriptiva y generación de números aleatorios, siendo la base de casi todas las librerías científicas en Python.
Su estructura fundamental no es una lista de Python, sino el ndarray (N-dimensional array). A diferencia de las listas tradicionales que son contenedores flexibles de punteros a objetos dispersos en memoria, un ndarray es un bloque de memoria contiguo que contiene elementos de un solo tipo de dato (homogéneos):
Eficiencia en Memoria: Al forzar que todos los datos sean del mismo tipo
Multidimensionalidad: Los arrays tienen “ejes” (axes) y una “forma” (shape). Un array puede representar desde un simple vector (1D) o una matriz (2D), hasta tensores de orden superior (3D, 4D…)
Vectorización, aumentando la velocidad de ejecución, las operaciones vectorizadas permiten aplicar funciones matemáticas a arrays enteros de una sola vez.
Pandas ofrece estructuras de datos como DataFrames (tablas) y Series (vectores), con herramientas para lectura/escritura de ficheros (CSV, Excel, SQL), limpieza de datos, transformaciones, agrupaciones y análisis exploratorio.
Es la biblioteca de referencia en el ecosistema de Python para la manipulación y análisis de datos estructurados.
El núcleo de Pandas se basa en dos estructuras de datos principales: las Series (unidimensionales, como una lista o una columna) y los DataFrames (bidimensionales, como una tabla).
SciPy es una librería de cómputo científico construida sobre NumPy, que añade módulos de alto nivel para optimización, integración numérica, estadística avanzada, procesamiento de señales, álgebra lineal y más. Es especialmente útil en contextos de ingeniería y ciencia aplicada, donde se necesitan algoritmos numéricos robustos como métodos de optimización, resolución de ecuaciones diferenciales o ajustes de modelos.
Matplotlib genera gráficos (líneas, barras, scatter, histogramas)** con control sobre estilos, colores, etiquetas y anotaciones, permite crear visualizaciones estáticas interactivas para reportes, presentaciones y análisis exploratorio de datos.
Es la librería más antigua para la visualización de datos en Python. Fue diseñada originalmente para imitar el comportamiento de graficado de MATLAB.
QuantLib es una librería especializada en valoración de derivados, calibración de curvas de tipos de interés, cálculo de volatilidad, análisis de riesgos y simulación de Monte Carlo.
import numpy as np# Array 1D (vector)array_1d = np.array([1, 2, 3, 4, 5])# Array 2D (matriz)array_2d = np.array([[1, 2, 3], [4, 5, 6]])# Array 3Darray_3d = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])# Array de ceroszeros = np.zeros((3, 4)) # 3 filas, 4 columnas# Array de unosones = np.ones((2, 3))# Array con un valor específicofull = np.full((2, 2), 7) # Matriz 2x2 llena de 7s# Array identidad (matriz diagonal con 1s)identity = np.eye(3)array_3d.shape # Dimensionesarray_3d.dtype # Tipo de datoarray_3d.size # Total de elementos# Indexación y slicingarray_1d[0] # Primer elementoarray_1d[-1] # Ultimo elementoarray_1d[1:4] # Elementos 1 a 3array_1d[::2] # Cada 2 elementosarray_2d[0, 1] # Fila 0, columna 1array_2d.reshape(3, 2) # Redimensionararray_2d.T # Transpuesta
Array N-dimensional (ndarray) de python. Al contrario que de las listas son homogéneos (mismo tipo), lo que permite un almacenamiento en memoria mucho más eficiente y operaciones matemáticas mas rapidas.
Array 1D (Vector): [1, 2, 3…] similar a una lista plana.
Array 2D (Matriz): Lista de listas o matrix.
Array 3D (Tensor): “Cubo” de datos.
Creación eficiente de arrays: np.zeros, np.ones(), np.full() y np.eye()
Metadatos: .shape, .dtype y .size
Indexación y slicing
Manipulación de la estructura .T y .reshape()
Numpy (2/3)
Operaciones aritméticas Vectorizadas (Element-wise), NumPy aplica las operación a todo el bloque de datos simultáneamente.
Filtros (Boolean Masking) para filtrar datos sin usar condicionales if.
Manipulación de Formas (Concatenate & Stack) unir arrays, esencial
Concatenate: Une vectores uno detrás de otro (crece en longitud).
Numeros pseudoaleatorios: fijar semillas np.random.seed(42), simulación de números aleatorios uniformes np.random.rand(5), normales estándar con np.random.randn(5), enteros con np.random.randint(0, 10, 5) y una matriz aleatoria 3x3 con np.random.rand(3, 3)
Pandas (1/5)
import pandas as pdserie1 = pd.Series([10, 20, 30, 40])serie2 = pd.Series([10, 20, 30, 40], index=['A', 'B', 'C', 'D'])serie2['B'] # 20 - Acceso por etiquetaserie2['A'] # 10# Crear serie desde diccionariodatos = {'España': 47, 'Francia': 67, 'Alemania': 83, 'Italia': 59}serie_paises = pd.Series(datos)# Atributos de pd.Serieserie_paises.sizeserie_paises.indexserie_paises.dtypeserie_paises.values# Dataframe# Desde diccionario donde cada clave es una columnadatos = {'Nombre': ['Alice', 'Bob', 'Carlos', 'Diana'],'Edad': [25, 30, 28, 35],'Salario': [50000, 60000, 55000, 70000],'Departamento': ['Ventas', 'IT', 'Finanzas', 'IT']}# Desde lista de datosdatos = [ ['Alice', 25, 50000], ['Bob', 30, 60000], ['Carlos', 28, 55000]]df = pd.DataFrame(datos, columns=['Nombre', 'Edad', 'Salario'])df.shapedf.columns # Nombre de columnasdf.index # RangeIndex(start=0, stop=3, step=1)df.dtypesdf.info() # Tipo de dato, valores no-nulosdf.head() # Por defecto, primeras 5 filasdf.head(2) # Primeras 2 filasdf.tail() # Últimas 5 filas
La Serie (pd.Series): Un Array con Etiquetas. A diferencia de un array de NumPy (que solo tiene posiciones numéricas 0, 1, 2…), la Serie permite definir un índice explícito (etiquetas como ‘A’, ‘B’, ‘España’).
El DataFrame (pd.DataFrame): Es una colección de Series alineadas (comparten el mismo índice), formando una tabla de filas y columnas. Se construye desde:
Diccionario: Clave = Nombre de Columna, Valor = Lista de datos.
Desde Lista de Listas.
Exploración rápida del dataframe:
.head() / .tail(): Muestra las primeras/últimas filas.
.info(): Muestra tipos de datos, uso de memoria y conteo de nulos.
.shape y .columns: Dimensiones (filas, columnas) y nombres de variables.
Pandas (2/5)
Selección de Datos: .loc vs .iloc
.loc (Label-based): Busca por etiqueta/nombre. df.loc[0, 'Nombre'] significa “Fila con índice 0 y Columna llamada ‘Nombre’”.
.iloc (Integer-based): Busca por posición numérica (como NumPy). df.iloc[0] es “La primera fila”, independientemente de cómo se llame su índice.
Filtrado Condicional (Queries): extraer subconjuntos de datos. Condiciones Lógicas: df[df['Edad'] > 28] devuelve solo las filas que cumplen la condición. Operadores Combinados: Uso de & (AND) y | (OR) para lógica compleja. Filtros de Texto: Métodos vectorizados de strings como .str.contains('li') o .isin([...]).
Transformación y Edición: nuevas columnas 2026 - df['Edad'] o un valor constante, .drop(..., axis=1): Elimina columnas. .rename(): renombra variables para mayor claridad. .sort_values(): Ordena los datos soporta múltiples niveles de ordenación.
df = pd.DataFrame({'Nombre': ['Alice', 'Bob', np.nan, 'Diana'],'Edad': [25, np.nan, 28, 35],'Salario': [50000, 60000, 55000, np.nan]})# Detectar valores faltantesprint(df.isna()) # Matriz booleanaprint(df.isnull()) # Alias para isna()# Contar valores faltantesprint(df.isna().sum()) # Por columna# Output:# Nombre 1# Edad 1# Salario 1# Eliminar filas con valores faltantesdf_limpio = df.dropna()print(df_limpio)# Rellenar valores faltantesdf_relleno = df.fillna(0) # Rellenar con 0df_relleno = df.fillna(df.mean()) # Rellenar con la media# Rellenar hacia adelante (forward fill)df_ffill = df.fillna(method='ffill')# Rellenar hacia atrás (backward fill)df_bfill = df.fillna(method='bfill')df = pd.DataFrame({'Edad': [25, 30, 28, 35, 32],'Salario': [50000, 60000, 55000, 70000, 62000]})# Estadísticas para una columnaprint(df['Edad'].mean()) # Media: 30.0print(df['Edad'].median()) # Mediana: 30print(df['Edad'].std()) # Desviación estándarprint(df['Edad'].var()) # Varianzaprint(df['Edad'].min()) # Mínimo: 25print(df['Edad'].max()) # Máximo: 35# Resumen estadístico completoprint(df.describe())# Contar valores únicosprint(df['Edad'].nunique()) # Número de valores únicos# Obtener valores únicosprint(df['Edad'].unique()) # [25 30 28 35 32]# Contar ocurrenciasprint(df['Edad'].value_counts())
Gestión de Datos Faltantes: df.isna().sum() ver rápidamente la calidad de tus datos. dropna borrar filas incompletas. fillna: rellenar los huecos para no perder información.
Estadística Descriptiva: Cálculo directo de mean (promedio), median (mediana), std (volatilidad) ignorando automáticamente los nulos. describe(): información estadistica del DataFrame (cuartiles, extremos, media).
Análisis de Frecuencias: value_counts() y nunique() ver qué valores son los más comunes o repetidos.
Pandas (4/5)
Agregación (groupby): Creación de grupos, para aplicar métricas .agg(['mean', 'min', 'max']) calcula varios estadisticas para cada grupo,
Concatenación (concat): unir pd.DataFrames
Vertical (Eje 0)
Horizontal (Eje 1): Añadir nuevas columnas a un dataset existente, asumiendo que el orden de las filas coincide.
Unión (merge): cruzar información de fuentes distintas usando una clave común (ID), replicando los JOIN de SQL.
Inner Join: Muestra solo lo que coincide en ambas tablas (intersección).
Left/Right/Outer Join: Dice qué hacer con los datos que no cruzan (preservar todo lo de la izquierda, todo lo de la derecha, o ambos).
import pandas as pd# Leer un CSVdf = pd.read_csv('datos.csv')# Guardar a CSVdf.to_csv('datos_procesados.csv', index=False)# Parámetros útilesdf = pd.read_csv('datos.csv', sep=';', # Separador (por defecto ',') encoding='utf-8', # Codificación nrows=1000) # Leer solo primeras 1000 filas# Leer Exceldf = pd.read_excel('datos.xlsx', sheet_name=0)# Guardar a Exceldf.to_excel('datos.xlsx', index=False)# Escribir múltiples hojaswith pd.ExcelWriter('datos.xlsx') as writer: df1.to_excel(writer, sheet_name='Hoja1', index=False) df2.to_excel(writer, sheet_name='Hoja2', index=False)# Leer JSONdf = pd.read_json('datos.json')# Guardar a JSONdf.to_json('datos.json', orient='records')df = pd.DataFrame({'Nombre': ['Alice', 'Bob', 'Carlos'],'Salario': [50000, 60000, 55000]})# Función personalizadadef categorizar_salario(salario):if salario <52000:return'Bajo'elif salario <58000:return'Medio'else:return'Alto'# Aplicar a una columnadf['Categoria'] = df['Salario'].apply(categorizar_salario)# Usar lambda (funciones anónimas)df['Salario_Ajustado'] = df['Salario'].apply(lambda x: x *1.1) # Aumentar 10%# Aplicar a toda la filadf['Nombre_Largo'] = df.apply(lambda row: row['Nombre'] +' ('+str(row['Salario']) +')', axis=1)df = pd.DataFrame({'Mes': ['Enero', 'Enero', 'Febrero', 'Febrero'],'Región': ['Norte', 'Sur', 'Norte', 'Sur'],'Ventas': [1000, 1500, 1200, 1800]})# Tabla dinámicapivot = df.pivot_table(values='Ventas', index='Mes', columns='Región', aggfunc='sum')
Input/Output Pandas es capaz de leer y escribir en los formatos más comunes de la industria: archivos de texto plano (CSV, JSON) hasta hojas de cálculo (Excel). Permite ajustar la lectura al archivo: definir separadores (sep=';'), o cargar solo una muestra (nrows=1000) para archivos grances.
Aplicación de funciones (apply) si las funciones predefinidas no son suficientes, apply permte aplicar funciones definidas por el usuario. Permite operar celda a celda o fila a fila (axis=1).
Pivot Tables tablas finámicas como en excel
QuantLib
Agenda:
Introducción a la programación en Python
Python
QuantLib
Valoración de derivados financieros
Modelo multifactorial de riesgo de crédito
Fechas / Contadores / Calendarios
import QuantLib as qltoday = ql.Date(15, 6, 2020) # Día, Mes, Año# Establecer como fecha de evaluación globalql.Settings.instance().evaluationDate = todayprint(ql.Settings.instance().evaluationDate) # June 15th, 2020# Sumar díasfuture_date = today +30# 30 días después# Sumar períodossix_months = today + ql.Period(6, ql.Months)six_months = today + ql.Period("6M")# Diferencia entre fechasdays_diff = (six_months - today)# Calendario: festivos de Estados Unidoscalendar = ql.UnitedStates(ql.UnitedStates.NYSE)# Contador de días: Actual/360 (mercados monetarios)dayCounter = ql.Actual360()# Contador de días: Actual/365 (bonos, derivados)dayCounter_365 = ql.Actual365Fixed()# Contador de días: 30/360 (mercados corporativos)dayCounter_30_360 = ql.Thirty360()# Contador de días: 30/360 (mercados corporativos)dayCounter_bus_252 = ql.Business252(calendar)# Calcular fracción de añostart_date = ql.Date(1, 1, 2020)end_date = ql.Date(30, 6, 2020)(end_date - start_date) /360year_fraction = dayCounter.yearFraction(start_date, end_date)calendar.businessDaysBetween(start_date, end_date) /252dayCounter_bus_252.yearFraction(start_date, end_date)
ql.Settings.instance().evaluationDate - fecha de referencia para descontar los flujos de caja futuros.
Fechas y operaciones con ql.Period()
Calendarios de Mercado los mercados no abren todos los días. Estos calendarios implemtan los festivos históricos y futuros de los principales meracados, como la bolsa de Nueva York, Londres, o TARGET en Europa.
Convenciones de Conteo de Días (DayCounters) Define cómo se mide un año en diferentes mercados, lo que determina el cálculo de intereses (devengo):
Actual/360: Estándar en mercados monetarios.
Actual/365: Usado en bonos gubernamentales y derivados.
30/360: Estándar en bonos corporativos de EE.UU. (asume todos los meses de 30 días para simplificar pagos).
Business/252: Usado en mercados emergentes como Brasil, donde solo cuentan los días hábiles (~252 al año).
¿Que día es festivo en EEUU segun el calendario de la Bolsa de Nueva York?
5 de Febrero de 2024
4 de Junio de 2024
15 de Agosto de 2024
26 de Noviembre de 2024
¿Cuántos dia abrió la bolsa de Nueva York en 2025?
250
251
252
253
254
Schedules (1/2)
ql.MakeSchedule en QuantLib es una función que simplifica la creación de objetos ql.Schedule (objeto para generar y gestionar la lista de fechas relevantes)
effectiveDate (ql.Date): punto de partida para generar las fechas.
terminationDate (ql.Date): Fecha de finalización.
tenor o frequency (ql.Period o ql.Frequency): determina cada cuánto tiempo se genera una fecha ql.Period('6M') o ql.Semiannual.
Parámetros Opcionales de ajuste
calendar (ql.Calendar): calendario de festivos
convention (ql.BusinessDayConvention): ajuste de fechas intermedias si caen en festivo.
terminationDateConvention (ql.BusinessDayConvention): Igual que convention, pero específico para la fecha de vencimiento. El vencimiento puede tener reglas diferentes a los cupones intermedios.
Schedules (2/2)
Su función principal es generar la lista de fechas de pago de un instrumento financiero (ej. cupones de un bono), aplicando automáticamente la lógica de calendarios y festivos.
Parámetros de Generación de Fechas (Rules)
Controlan la de generación antes de ajustar por festivos.
rule (ql.DateGeneration.Rule): Backward (Default): desde la fecha de vencimiento hacia atrás, Forward: desde la fecha de inicio hacia adelante, Zero, ThirdWednesday.
endOfMonth (bool): si la fecha de inicio es fin de mes, todas las fechas subsiguientes sean también fin de mes.
Parámetros para Periodos Irregulares
firstDate (ql.Date): fuerza una fecha específica para la primera fecha/cupón.
nextToLastDate (ql.Date): fuerza una fecha específica para el penúltimo cupón.
Usando ql.MakeSchedule() y calendario ql.TARGET. En el calendario aparece una fecha que cae en sábado 31/05/2025. Si QuantLib ajusta las fechas con la convención ql.ModifiedFollowing, ¿a qué fecha laborable se moverá?
Lunes 02/06/2025
Viernes 30/05/2025
Sábado 31/05/2025 (no se ajusta)
Jueves 29/05/2025
Curvas de tipos
Una curva de tipos de interés (yield curve) es una función que muestra la relación entre el plazo de vencimiento y la tasa de interés de descuento. Permite:
Valorar cualquier instrumento de renta fija (bonos, swaps, derivados)
Derivar los forward implícitos (expectativas del mercado sobre tasas futuras)
Las curvas de tipos normalmente se obtienen a través de:
Bootstrapping: Construye una curva que reprecia exactamente los instrumentos de mercado. Usa interpolación local (lineal, cúbica). Ver (Ametrano & Bianchetti, 2013).
Fitting: Ajusta parámetros de un modelo paramétrico (Nelson-Siegel, Svensson) minimizando diferencias de precios. Produce curvas más suaves pero aproximadas.
import QuantLib as qltoday = ql.Date(15, 6, 2020)ql.Settings.instance().evaluationDate = today# Parámetrossettlement_days =2calendar = ql.UnitedStates(ql.UnitedStates.NYSE)rate =0.05day_counter = ql.Actual360()# Crear curva planaflat_forward = ql.FlatForward(settlement_days, calendar, rate, day_counter)flat_forward.referenceDate()flat_forward.discount(today + ql.Period(1, ql.Years))# Definir puntos de la curvadates = [ql.Date(15, 6, 2020), ql.Date(15, 12, 2020), ql.Date(15, 6, 2021), ql.Date(15, 6, 2022)]yields = [0.01, 0.015, 0.02, 0.025] # Tasas en cada fechaday_counter = ql.Actual360()# Crear curva con interpolación linealcurve = ql.LogLinearZeroCurve(dates, yields, day_counter)# Handle para usar la curvacurve_handle = ql.YieldTermStructureHandle(curve)# Obtener tasa forward a una fechatarget_date = ql.Date(15, 9, 2020)forward_rate = curve_handle.forwardRate(target_date, target_date, day_counter, ql.Simple)forward_rate.rate()
Valoración de instrumentos
La libreria QuantLib valora los instrumentos financieros a través de dos piezas:
Instrumento (ql.Instrument): qué contrato tengo (bono, swap, opción, …) y que características contractuales tengo (fechas, cupones, strike, …).
Pricing engine (ql.PricingEngine): con qué modelo y con qué datos de mercado lo valoro.
Y se conectan a través de .setPricingEngine(engine):
Un instrumento es un objeto que representa un producto financiero concreto. Los parámetros fijos del contrato:
Bonos: nominal, cupón, fechas de pago…
Swaps: notional, tipo fijo, índice flotante, calendarios…
Opciones: tipo (call/put), strike, fecha de vencimiento…
Pricing engine
Un pricing engine es un objeto que define:
Qué modelo usar, dependiendo del ql.Instrument (descuento de flujos, Black‑Scholes, árbol binomial, …)
Qué datos de mercado usar (curvas, volatilidades, spot…)
El Bono
Un bono es esencialmente un préstamo troceado en pequeñas partes que cotizan en un mercado. Cuando un inversor compra un bono, está prestando dinero a una entidad (Gobierno o empresa) a cambio de recibir unos intereses periódicos y la devolución del dinero en el futuro.
Aunque se denomine “renta fija”, su precio no es fijo, cambia constantemente en el mercado; lo que es “fijo” son las condiciones del contrato (fechas de pago y fórmula del cupón).
El precio de un bono no es más que la suma de todo el dinero que van a pagar en el futuro, traído a valor presente (descontado) al día de hoy.
Donde, \(P\): Precio actual del bono, \(C\): Cupón, \(N\): Nominal, \(r\): Tasa de interés de mercado, \(n\): Número de periodos hasta el vencimiento, \(t\): Periodo de tiempo específico de cada flujo
El Entorno (Settings): ql.Settings.instance().evaluationDate = today. Especificar la fecha de valoración
El Instrumento (ql.FixedRateBond). El bono no sabe nada de tipos de interés, solo sabe que cupones tiene comprometidos.
settlement_days: Días que tardamos en liquidar (normalmente 2 en Europa/USA).
day_count: Convención para contar días (30/360, Actual/360, etc.). Importante para calcular el cupón corrido.
FixedRateBond: Es la clase para bonos estándar. Si fuera flotante, usaríamos FloatingRateBond.
Datos de mercado (ql.YieldTermStructureHandle). Valor de los datos de mercado hoy.
La Curva: Usamos ql.FlatForward por simplicidad.
El Handle: Esto permite que si cambiamos la tasa de la curva más tarde, el bono se actualice automáticamente
Price engine (ql.DiscountingBondEngine). Como valorar, descontando los flujos de caja que genera el bono usando la curva que le hemos dado en el Handle.
bond.setPricingEngine(engine): definimos el engine en el instrumento.
Resultados:
NPV (Net Present Value): Es el valor total del bono hoy. Normalmente coincide con el Dirty Price.
Dirty Price (Precio Sucio): Precio que realmente pagas. Incluye el principal + el cupón corrido (intereses generados desde el último cupón hasta hoy).
Clean Price (Precio Limpio): El precio que ves en las pantallas de Bloomberg/Reuters. Es el Dirty Price - Cupon corrido.
Yield (TIR): QuantLib tiene un método inverso (bondYield). Le das el precio sucio y te devuelve la TIR.
¿Qué otras formas alternativas hay de obtener financiación?
¿Qué son las cédulas hipotecarias?
¿Por que los bancos emiten cédulas?
import QuantLib as ql# --- PASO 1: Configurar el entorno ---today = ql.Date(15, 2, 2025)ql.Settings.instance().evaluationDate = today# --- PASO 2: Definir el Contrato ---# 2.1 Crear el calendario de pagos (Schedule)schedule = ql.MakeSchedule(effectiveDate=ql.Date(15, 2, 2024), terminationDate=ql.Date(15, 2, 2031), tenor=ql.Period(ql.Annual), calendar=ql.TARGET(), convention=ql.Following, terminationDateConvention=ql.Following, rule=ql.DateGeneration.Backward, endOfMonth=False)# 2.2 Crear el Bonocoupons = [0.03] # 3% cupón anualday_count = ql.ActualActual(ql.ActualActual.ISMA)fixed_rate_bond = ql.FixedRateBond(2,100, schedule, coupons, day_count)# --- PASO 3: Definir datos de mercado (La Curva) ---rate =0.04curve_day_count = ql.Actual360()curve_handle = ql.YieldTermStructureHandle(ql.FlatForward(today, rate, curve_day_count))# --- PASO 4: Pricing Engine ---bond_engine = ql.DiscountingBondEngine(curve_handle)# Definir engine en el bonofixed_rate_bond.setPricingEngine(bond_engine)# --- RESULTADOS ---print(f"NPV (Valor Presente): {fixed_rate_bond.NPV():.4f}")print(f"Clean Price: {fixed_rate_bond.cleanPrice():.4f}")print(f"Dirty Price: {fixed_rate_bond.dirtyPrice():.4f}")print(f"Accrued Amount (Cupón corrido): {fixed_rate_bond.accruedAmount():.4f}")# Cálculo de la TIR dado un precio de mercadomarket_price =95.0yield_rate = fixed_rate_bond.bondYield( market_price, day_count, ql.Compounded, ql.Annual)print(f"Yield (TIR) para precio {market_price}: {yield_rate:.2%}")
Ejercicio: Valoración de un bono
Estamos a 17 de febrero de 2026 y tenemos que valorar la Obligación a 15 años que el Tesoro español acaba de emitir en enero, BOE. Suponiendo que los tipos interes son de 3% y la estructura temporal es plana.
Calcular con QuantLib:
El Precio Sucio (Dirty Price).
El Precio Limpio (Clean Price).
El Cupón Corrido (Accrued Interest).
Datos del Bono según el BOE:
Tipo: Obligación del Estado a 15 años.
Vencimiento: 31 de enero de 2041.
Cupón: 3.50% anual.
Pago de cupón: 31 de enero de cada año.
Convención: Actual/Actual (ICMA).
Fecha de liquidación: T+2 (Estándar mercado español).
Ejercicio: Valoración de un bono
import QuantLib as qlvaluation_date = ql.Date(17, 2, 2026)ql.Settings.instance().evaluationDate = valuation_date# Definición del Instrumento (Datos del BOE)issue_date = ql.Date(31, 1, 2026) # Asumimos fecha de origen teórica coincidente con cupónmaturity_date = ql.Date(31, 1, 2041)coupon_rate =0.035# 3.50%nominal =100.0# Calendario y Convenciones (Estándar España)calendar = ql.TARGET()day_count = ql.ActualActual(ql.ActualActual.ISMA)coupon_frequency = ql.Period(ql.Annual)settlement_days =2# T+2# Construcción del Calendario de Pagos (Schedule)schedule = ql.MakeSchedule( effectiveDate=issue_date, terminationDate=maturity_date, tenor=coupon_frequency, calendar=calendar, convention=ql.ModifiedFollowing, rule=ql.DateGeneration.Backward, endOfMonth=False)# Construcción del Objeto Bonobond = ql.FixedRateBond( settlement_days, nominal, schedule, [coupon_rate], day_count)# 3. Creación de la Curva# Estructura Temporal Plana (FlatForward)ts_curve = ql.FlatForward( valuation_date, 0.03, ql.Actual365Fixed(), ql.Compounded, ql.Annual)ts_handle = ql.YieldTermStructureHandle(ts_curve)# 4. Motor de Descuento (Pricing Engine)bond_engine = ql.DiscountingBondEngine(ts_handle)bond.setPricingEngine(bond_engine)# 5. Resultadosdirty_price = bond.dirtyPrice()clean_price = bond.cleanPrice()accrued = bond.accruedAmount()
Precios del bono
El Swap
Un Swap no es más que intercambiar una serie de pagos normalmente unos fijos por otros variables. Para valorarlo, hay que hacer dos cosas:
Proyectar: Ver cuánto estima el mercado que valdrá el subyacente en el futuro (usando la curva para calcular los forward).
Descontar: Trae esos flujos futuros a valor presente (usando la curva de descuento).
El valor de un Swap (\(V_{swap}\)) para quien paga fijo y recibe flotante (Payer) es la diferencia entre el valor presente de la pata flotante (\(PV_{float}\)) y el valor presente de la pata fija (\(PV_{fixed}\)):
\[
V_{swap} = PV_{float} − PV_{fixed}
\]
donde:
\[
PV_{fixed} = N K \sum_{i=1}^n \tau_i DF_i
\]
\(N\): Nominal,
\(K\): Tasa fija acordada,
\(\tau_i\): Fracción de año para el periodo \(i\).
\(DF(T_i)\): Factor de descuento del pago \(i\).
\[
PV_{float} = N \sum_{i=1}^n F(t_j, T_j) \delta_i DF_j
\]
\(F(t_j, T_j)\): tasa Forward entre \(t_j\) y \(T_j\).
\(\delta_j\): Fracción de año de la parte flotante.
\(DF_j\): Factor de descuento del pago \(j\)
El Entorno (Settings): ql.Settings.instance().evaluationDate = today. Especificar la fecha de valoración.
Crear Índice subyacente (ql.Euribor6M).
ql.Schedule: dos, un Swap suele tener dos frecuencias distintas. Es muy común:
Pata Fija: Anual (pagas una vez al año).
Pata Flotante: Semestral (o Trimestral), coincidiendo con la frecuencia del índice (Euribor 6M o 3M).
El Instrumento (VanillaSwap)
Definimos si somos Payer (pagamos fijo) o Receiver (recibimos fijo), nominal, schedules de la pata fija y variable, índice subyacente y el spread.
ql.VanillaSwap.Payer: “Payer of Fixed”. Por tanto, pagamos el 3% fijo y cobramos Euribor. Alternativamente ql.VanillaSwap.Receiver ``
PricingEngine
ql.DiscountingSwapEngine al que le pasamos la curva de descuento
Resultados
A diferencia del bono, donde miramos el precio o la TIR, en el Swap lo más importante suele ser el Fair Rate.
fairRate(): Es la tasa fija que haría que el NPV del swap fuera exactamente CERO hoy.
fairSpread(): Es el spread que añadido al EURIBOR 6M haría el NPV del swap CERO.
import QuantLib as ql# --- PASO 1: Fecha de valoración ---today = ql.Date(15, 2, 2026)ql.Settings.instance().evaluationDate = todaycalendar = ql.TARGET()# --- PASO 2: Curva e Índices ---rate =0.03curve_day_count = ql.Actual365Fixed()curve_handle = ql.YieldTermStructureHandle( ql.FlatForward(today, rate, curve_day_count))# 2.2 El Índice (Euribor 6M)euribor6m = ql.Euribor6M(curve_handle)# --- PASO 3: El Contrato (Swap) ---start_date = ql.Date(17, 2, 2026) # Spot (T+2)maturity = ql.Date(17, 2, 2031) # 5 añosnominal =10_000_000.0# 10 Millones# 3.1 Calendario Pata Fija (Anual)fixed_schedule = ql.MakeSchedule( effectiveDate=start_date, terminationDate=maturity, tenor=ql.Period(ql.Annual), calendar=calendar, convention=ql.ModifiedFollowing, terminationDateConvention=ql.ModifiedFollowing, rule=ql.DateGeneration.Forward, endOfMonth=False)# 3.2 Calendario Pata Flotante (Semestral, igual que el Euribor 6M)float_schedule = ql.MakeSchedule( effectiveDate=start_date, terminationDate=maturity, tenor=ql.Period(ql.Semiannual), calendar=calendar, convention=ql.ModifiedFollowing, terminationDateConvention=ql.ModifiedFollowing, rule=ql.DateGeneration.Forward, endOfMonth=False)# --- PASO 5: Construir el Swap ---fixed_rate =0.03# Pagamos fijo al 3%spread =0.0# Spread sobre Euriborir_swap = ql.VanillaSwap( ql.VanillaSwap.Payer, # Nosotros PAGAMOS fijo, RECIBIMOS flotante nominal, fixed_schedule, fixed_rate, ql.Thirty360(ql.Thirty360.BondBasis), # DayCount Fijo float_schedule, euribor6m, spread, ql.Actual360() # DayCount Flotante)# --- PASO 5: Pricing Engine ---engine = ql.DiscountingSwapEngine(curve_handle)ir_swap.setPricingEngine(engine)# --- RESULTADOS ---print(f"NPV (Valor Presente Neto): {ir_swap.NPV():,.2f}")print(f"Fair Rate (Tasa de mercado): {ir_swap.fairRate():.4%}")print(f"Fair Spread: {ir_swap.fairSpread():.4%}")# Análisis de las patas por separado (opcional)print(f"NPV Pata Fija (lo que pago): {ir_swap.fixedLegNPV():,.2f}")print(f"NPV Pata Flotante (lo que recibo): {ir_swap.floatingLegNPV():,.2f}")
En el ejemplo anterior, si la curva de tipos sube al 4%, ¿gano o pierdo dinero?
Gano
Pierdo
Como soy Payer (pago fijo al 3% y recibo variable), si los tipos suben al 4%, recibiré pagos variables más altos mientras sigo pagando mi fijo al 3%. Por lo tanto NPV subirá.
Valoración de derivados financieros
Agenda:
Introducción a la programación en Python
Python
QuantLib
Valoración de derivados financieros
Modelo multifactorial de riesgo de crédito
Valoración de derivados: moneda
Imaginemos que queremos valorar y estudiar los riesgos asociados a un contrato financiero cuyo valor final depende del resultado del lanzamiento de la moneda (\(\omega\)) pagamos un 1€ si sale cara y recibimos 1€ si sale cruz. Nosotros como creadores de mercado (market-makers) ofrecemos precios de compra o venta. El espacio muestral es \(\Omega = {H, T}\) (Cara, Cruz).
El primer paso es definir el Payoff (\(V\)), una variable aleatoria al final en \(t_1\):
Si sale Cara (\(\omega = H\)): \(V(H) = -1\) (Pagas 1 euro).
Si sale Cruz (\(\omega = T\)): \(V(T) = +1\) (Recibes 1 euro).
El valor teórico del contrato en \(t_0\) (\(V_0\)) se define como la esperanza matemática de sus flujos de caja futuros, descontados a la tasa libre de riesgo (\(r\)).
¿Todo lo que me paguen por encima de su valor teórico es beneficio? ¿Cuales son los riesgos asociados a este contrato?
Mercado: el resultado de la variable subyacente no sea favorable.
Concentración: muchos contratos con el mismo signo (corto o largo).
Contraparte: siendo el resultado favorable, el comprador no pagua.
Wrong Way Risk: correlación entre la exposición y la calidad crediticia.
Modelo: y si la formulación no es correcta y la probabilidad de cara no es 50%.
Valoración: opcion europea (1/6)
Cambiamos el contrato, ahora compramos una Opción Call Europea sobre una acción (\(S_t\)) con precio de ejercicio (Strike) de 100€ y vencimiento en 1 año. El primer paso es definir el Payoff (\(V\)), una variable aleatoria al vencimiento en \(t_1\):
Si en \(t=1\) la acción vale mas de 100€, recibire \(S_1 - 100\).
Si en \(t=1\) la acción vale menos de 100€, no recibire nada.
Por lo tanto el payoff en el vencimiento es \(max(S_1 - K, 0)\). ¿Pero cuanto vale este contrato hoy?
Al contrario que con la moneda, no conocemos la distribución de probabildad del activo subyacente… Por lo tanto, nos toca hacer una asunción. Por ejemplo, supongamos que modelizamos \(S_t\) a través de un “Movimiento Browniano Geométrico”, su ecuación diferencial seria:
\[
dS_t = \mu S_t dt + \sigma S_t dW_t
\]
donde (\(S_t\)) es el precio del activo, (\(\mu\)) es la deriva o “drift”, (\(\sigma\)) is the volatility, and (\(W_t\)) is a Wiener process.
El proceso de Wiener se caracteriza por las siguientes propiedades:
\(W_0\) = 0
Los incrementos de \(W\) son independientes
Los incrementos de \(W\) son normales con media 0 y varianza \(u\).
La ecuación diferencial del movimiento geométrico browniano es una ecuación es continua. Pero podemos aproximarla (discretizarla) a través del método de Euler (Discretización de Euler-Maruyama). Que consiste en aproximar los diferenciales \(dS\), \(dt\), \(dW\) por incrementos pequeños \(\Delta S\), \(\Delta t\) y \(\Delta W\).
Donde el incremento browniano \(\Delta W_t\) se simula como \(\Delta W_t = \sqrt{\Delta t} Z\) con \(Z \sim N(0,1)\):
Disclaimer
Aunque esta fórmula (Euler directa) parece lógica, tiene un problema grave: no garantiza que el precio sea positivo. Si \(\sigma \Delta W_t\) es un número negativo muy grande, \(S_{t + \Delta t}\) puede volverse negativo, lo cual es imposible para una acción.
La solución exacta (Discretización Logarítmica)
Para evitar precios negativos y ser más precisos, aplicamos el Lema de Itô a \(ln(S_t)\). Lo que nos llevaria a la siguiente solución:
\[
S_t = S_0 e^\left((\mu - \frac{1}{2} \sigma^2) \Delta t + \sigma W_t\right) = S_0 e^\left((\mu - \frac{1}{2} \sigma^2) \Delta t + \sigma \Delta t Z \right)
\]
Simulación de un camino del movimiento browniano geométrico aplicando la discretización de Euler-Maruyama.
import numpy as npimport matplotlib.pyplot as plt# ParametersS0 =100# initial asset pricemu =0.05# driftsigma =0.2# volatilityT =1.0# time horizon (year)steps =252# trading days in one yeardt = T / steps# Simulate one GBM path# np.random.seed(42)t = np.linspace(0, T, steps)W = np.random.standard_normal(size=steps)W = np.cumsum(W) * np.sqrt(dt)S = S0 * np.exp((mu -0.5* sigma**2) * t + sigma * W)plt.figure(figsize=(8,4))plt.plot(t, S)plt.title('Simulated Geometric Brownian Motion Path')plt.xlabel('Time (years)')plt.ylabel('Price')plt.grid(True)plt.show()
Valoración: opcion europea (4/6)
Simulación de multiples caminos del movimiento browniano geométrico aplicando la discretización de Euler-Maruyama.
import numpy as npimport matplotlib.pyplot as plt# ParametersS0 =100# initial asset pricemu =0.05# driftsigma =0.2# volatilityT =1.0# time horizon (year)steps =252# trading days in one yeardt = T / stepsn_paths =100# number of simulated pathst = np.linspace(0, T, steps)paths = np.zeros((n_paths, steps))for i inrange(n_paths): W = np.random.standard_normal(size=steps) W = np.cumsum(W) * np.sqrt(dt) S = S0 * np.exp((mu -0.5* sigma**2) * t + sigma * W) paths[i, :] = Splt.figure(figsize=(10, 5))for i inrange(n_paths): plt.plot(t, paths[i, :], lw=0.8, alpha=0.6)plt.title('Simulated Geometric Brownian Motion Paths')plt.xlabel('Time (years)')plt.ylabel('Price')plt.grid(True)plt.show()
Valoración: opcion europea (5/6)
Por lo tanto valoraremos el precio de la opción como la esperanza matemática de sus flujos de caja futuros, descontados a la tasa libre de riesgo \(r\).
K =100# strike pricer =0.05# risk-free rateN =10000# number of pathspayoffs = np.zeros(N)for i inrange(N): W = np.random.standard_normal(size=steps) W = np.cumsum(W) * np.sqrt(dt) S_T = S0 * np.exp((mu -0.5*sigma**2)*T + sigma * W[-1]) payoffs[i] =max(S_T - K, 0)option_price_mc = np.exp(-r * T) * np.mean(payoffs)print(f"Monte Carlo Price: {option_price_mc:.4f}")
import QuantLib as qlimport numpy as np# 1. Configuración de parámetrosS0 =100.0# Precio inicial del activoK =100.0# Precio de ejercicio (Strike)r =0.05# Tasa libre de riesgosigma =0.2# VolatilidadT =1.0# Tiempo hasta vencimiento (años)div_yield =0.0# Rentabilidad por dividendos (asumimos 0)calculation_date = ql.Date.todaysDate()maturity_date = calculation_date + ql.Period(int(T*365), ql.Days)ql.Settings.instance().evaluationDate = calculation_date# 2. Construcción del Proceso Estocástico (Black-Scholes-Merton)spot_handle = ql.QuoteHandle(ql.SimpleQuote(S0))rate_handle = ql.YieldTermStructureHandle(ql.FlatForward(calculation_date, r, ql.Actual365Fixed()))div_handle = ql.YieldTermStructureHandle(ql.FlatForward(calculation_date, div_yield, ql.Actual365Fixed()))vol_handle = ql.BlackVolTermStructureHandle(ql.BlackConstantVol(calculation_date, ql.TARGET(), sigma, ql.Actual365Fixed()))bsm_process = ql.BlackScholesMertonProcess(spot_handle, div_handle, rate_handle, vol_handle)# 3. Definición de la Opciónpayoff = ql.PlainVanillaPayoff(ql.Option.Call, K)exercise = ql.EuropeanExercise(maturity_date)european_option = ql.EuropeanOption(payoff, exercise)# 4. CÁLCULO ANALÍTICO (Fórmula Black-Scholes)european_option.setPricingEngine(ql.AnalyticEuropeanEngine(bsm_process))price_analytic = european_option.NPV()print(f"Precio Black-Scholes: {price_analytic:.6f}")
Calculo Numerico (Monte Carlo):
import QuantLib as qlimport numpy as np# 1. Configuración de parámetrosS0 =100.0# Precio inicial del activoK =100.0# Precio de ejercicio (Strike)r =0.05# Tasa libre de riesgosigma =0.2# VolatilidadT =1.0# Tiempo hasta vencimiento (años)div_yield =0.0# Rentabilidad por dividendos (asumimos 0)calculation_date = ql.Date.todaysDate()maturity_date = calculation_date + ql.Period(int(T*365), ql.Days)ql.Settings.instance().evaluationDate = calculation_date# 2. Construcción del Proceso Estocástico (Black-Scholes-Merton)spot_handle = ql.QuoteHandle(ql.SimpleQuote(S0))rate_handle = ql.YieldTermStructureHandle(ql.FlatForward(calculation_date, r, ql.Actual365Fixed()))div_handle = ql.YieldTermStructureHandle(ql.FlatForward(calculation_date, div_yield, ql.Actual365Fixed()))vol_handle = ql.BlackVolTermStructureHandle(ql.BlackConstantVol(calculation_date, ql.TARGET(), sigma, ql.Actual365Fixed()))bsm_process = ql.BlackScholesMertonProcess(spot_handle, div_handle, rate_handle, vol_handle)# 3. Definición de la Opciónpayoff = ql.PlainVanillaPayoff(ql.Option.Call, K)exercise = ql.EuropeanExercise(maturity_date)european_option = ql.EuropeanOption(payoff, exercise)# 4. CÁLCULO NUMÉRICO (Monte Carlo)steps =1N =100000# Número de simulacionesmc_engine = ql.MCEuropeanEngine(bsm_process, "PseudoRandom", timeSteps=steps, requiredSamples=N)european_option.setPricingEngine(mc_engine)price_mc = european_option.NPV()print(f"Precio Monte Carlo: {price_mc:.6f}")
Modelo multifactorial de riesgo de crédito
Agenda:
Introducción a la programación en Python
Python
QuantLib
Valoración de derivados financieros
Modelo multifactorial de riesgo de crédito
Modelo de Merton (1/3)
(Merton, 1974) introdujo los modelos estructurales de riesgo de crédito. La idea básica es ver el default de una empresa como un problema de opciones sobre el valor de sus activos totales. Considerando:
\(A_t\): valor de los activos de la empresa en \(t\).
\(D\): valor nominal de la deuda que vence en el horizonte \(T\).
\(E_t\): valor de mercado del equity.
¿Cual es la condición de default?
\[
A_T < D
\]
En ese caso, los accionistas entregan la empresa a los bonistas y su payoff es cero. Por el contrario, si \(A_t \ge D\), el payoff de los accionistas es \(A_T − D\).
El equity es una call sobre los activos con strike \(D\).
Modelo de Merton (2/3)
El supuesto es que los valor de los activos \(A_t\) siguen un movimiento browniano geométrico:
El supuesto es que los valor de los activos \(A_t\) siguen un movimiento browniano geométrico:
\[
\frac{dA_t}{A_t} = \mu_A dt + \sigma_A dW_t
\]
https://dangulo.shinyapps.io/merton_model/
Bajo los supuestos de Black–Scholes–Merton, \(A_T\) sigue una distribución lognormal. Por lo tanto podemos escribir la probabilidad de default (bajo la medida elegida) como:
Ametrano, F. M., & Bianchetti, M. (2013). Everything you always wanted to know about multiple interest rate curve bootstrapping but were afraid to ask. SSRN Electronic Journal, (2219548). https://papers.ssrn.com/sol3/papers.cfm?abstract_id=2219548